昨天我們完成了 ICMP Echo Reply,讓我們的 Network Stack 具備了主機端(Endpoint)回應 Ping 的能力。
今天我們要邁向路由器(Router)的核心思維:生命週期限制(Time-to-Live, TTL) 與 ICMP Time Exceeded (Type = 11)!
同時,我們將深入剖析網路工程師每日必用的 traceroute 診斷工具背後的真正原理。
角色轉換提醒:Day 8~Day 9 我們把 Network Stack 當成「終端主機(Endpoint)」使用,主要處理目的地就是自己的封包;從 Day 10 開始,我們開始讓它學習「路由器(Router)」行為。真正的 Router 只會在**轉發(Forwarding)**封包時扣減 TTL;若封包目的地就是本機,應走 Local Delivery,不需要扣 TTL。Day 10 先用簡化模型觀察 TTL Expired,Day 11 會再加入目的地判斷,讓 Local Delivery 與 Forwarding 決策更清楚。

完成以下功能與架構演進:
ipv4_decrement_ttl():模擬路由器跳轉(Hop-by-Hop)行為,對進來的 IPv4 封包執行 TTL 扣減與過期檢查。traceroute 運作原理:徹底理解 traceroute 如何透過故意遞增 TTL(TTL=1, 2, 3...)迫使沿途路由器依序報錯,進而繪製出端到端路徑。tap.c 的 Echo Reply 邏輯與 Time Exceeded 一同封裝進 src/icmp.c(實作 icmp_receive() 與 icmp_send_time_exceeded()),使 tap.c 回歸純粹的網卡驅動與 EtherType 分流器。在複雜的互聯網拓撲中,若兩台路由器之間因為靜態路由設定錯誤或動態路由協定收斂延遲,形成互指迴圈:
┌─────────────┐
│ Router A │◄────────────┐
└──────┬──────┘ │
│ │
▼ │
┌─────────────┐ │ (Looping Forever!)
│ Router B │─────────────┘
└─────────────┘
TTL 扣減是 Forwarding Path 的責任,而不是 Local Delivery 的責任。也就是說,當 Router 收到「不是給自己、需要幫忙轉送」的封包時,才會先做 TTL--。
當 Router 在轉發路徑收到 TTL == 1 的封包時:
TTL-- 變為 0。發送端 (10.0.0.1) 我們的 Router (10.0.0.2)
│ │
├──── 1. 送出封包 (TTL = 1) ──────────────────────────►│
│ │ (TTL-- 變成 0 -> DROP!)
│ │
│◄─── 2. ICMP Time Exceeded (Type 11, Code 0) ─────────┤
▼ ▼
收到警告訊息:
"From 10.0.0.2: Time to live exceeded"
假設發送端主機同時在做很多事情:
當發送端收到一個 ICMP Time Exceeded 時,它怎麼知道是哪一個程式送的封包死掉了?
答案是:RFC 792 規定,所有 ICMP 差錯報文(Error Message)必須附帶:
┌─────────────────┬──────────────────────────────────────────────┐
│ ICMP Header │ Type=11, Code=0, Checksum, Unused (4 bytes) │
├─────────────────┼──────────────────────────────────────────────┤
│ │ 原始封包的 IPv4 Header (20 bytes) │
│ ICMP Payload │ + │
│ │ 原始封包 Payload 的前 8 bytes (如 TCP/UDP 埠號│
│ │ 或 ICMP ID/Seq) │
└─────────────────┴──────────────────────────────────────────────┘
因為 TCP 和 UDP 的 Source Port / Destination Port 正好位在 Payload 的前 4 個 bytes,ICMP Echo 的 ID 和 Seq 也位在前 4 個 bytes。
附帶這 8 個 bytes,發送端核心就能透過來源與目的 Port 精準找到到底是哪一個 Socket、哪一個 Process 的封包逾期!
traceroute 的本質traceroute 根本不需要路由器有任何特異功能,它僅僅是利用了 TTL 扣減的標準行為:

include/ipv4.h 與 src/ipv4.c:實作 TTL 遞減檢查在 include/ipv4.h 宣告:
int ipv4_decrement_ttl(struct ipv4_hdr *ip);
在 src/ipv4.c 實作:
int ipv4_decrement_ttl(struct ipv4_hdr *ip)
{
if (ip->ttl == 0)
return -1;
ip->ttl--;
if (ip->ttl == 0)
return -1;
return 0;
}
注意:這個函式目前只負責 TTL 數值扣減與過期判斷。若封包接下來真的要被轉發出去,因為 IPv4 Header 已被修改,送出前必須將
ip->checksum = 0並重新計算 IPv4 Header Checksum。Day 10 的重點是觀察 TTL Expired 與 ICMP Time Exceeded;完整 forwarding datapath 會在後續再逐步補齊。
include/icmp.h:定義常數與介面宣告在 include/icmp.h 中:
#define ICMP_ECHO_REPLY 0
#define ICMP_ECHO_REQUEST 8
#define ICMP_TIME_EXCEEDED 11 // Type 11: Time Exceeded
struct icmp_hdr {
uint8_t type;
uint8_t code;
uint16_t checksum;
uint16_t identifier;
uint16_t sequence;
} __attribute__((packed));
void icmp_print_header(const struct icmp_hdr *icmp, size_t length);
int icmp_verify_checksum(const void *icmp, size_t length);
void icmp_handle(uint8_t *packet, size_t length);
// 模組化介面:接收並處理一般 ICMP 封包 (Echo Reply)
void icmp_receive(int fd, const uint8_t *frame, size_t len);
// 模組化介面:發送 ICMP Time Exceeded 報錯封包
void icmp_send_time_exceeded(int fd, const uint8_t *orig_frame, size_t orig_len);
src/icmp.c:組裝發送 Time Exceeded 與封裝 Echo Reply在 src/icmp.c 中實作兩個核心函式:
void icmp_send_time_exceeded(int fd, const uint8_t *orig_frame, size_t orig_len)
{
if (orig_len < ETH_HEADER_LEN + sizeof(struct ipv4_hdr))
return;
const struct ethernet_hdr *orig_eth = (const struct ethernet_hdr *)orig_frame;
const struct ipv4_hdr *orig_ip = (const struct ipv4_hdr *)(orig_frame + ETH_HEADER_LEN);
uint8_t orig_ihl = (orig_ip->version_ihl & 0x0F) * 4;
// 計算附帶的原封包資料長度:原 IP 標頭 + 原 Payload 前 8 bytes
size_t orig_ip_payload_len = orig_len - ETH_HEADER_LEN - orig_ihl;
size_t copy_payload_len = (orig_ip_payload_len >= 8) ? 8 : orig_ip_payload_len;
size_t icmp_data_len = orig_ihl + copy_payload_len;
size_t icmp_total_len = sizeof(struct icmp_hdr) + icmp_data_len;
size_t reply_frame_len = ETH_HEADER_LEN + sizeof(struct ipv4_hdr) + icmp_total_len;
uint8_t reply[2048];
memset(reply, 0, reply_frame_len);
struct ethernet_hdr *eth_out = (struct ethernet_hdr *)reply;
struct ipv4_hdr *ip_out = (struct ipv4_hdr *)(reply + ETH_HEADER_LEN);
struct icmp_hdr *icmp_out = (struct icmp_hdr *)(reply + ETH_HEADER_LEN + sizeof(struct ipv4_hdr));
uint8_t *icmp_payload = reply + ETH_HEADER_LEN + sizeof(struct ipv4_hdr) + sizeof(struct icmp_hdr);
// 1. 交換 MAC 地址
memcpy(eth_out->dst, orig_eth->src, ETH_ADDR_LEN);
memcpy(eth_out->src, orig_eth->dst, ETH_ADDR_LEN);
eth_out->ethertype = htons(ETHERTYPE_IPV4);
// 2. 組裝全新 IPv4 標頭
ip_out->version_ihl = 0x45;
ip_out->tos = 0;
ip_out->total_length = htons(sizeof(struct ipv4_hdr) + icmp_total_len);
ip_out->identification = htons(0);
ip_out->flags_fragment = 0;
ip_out->ttl = 64;
ip_out->protocol = IPPROTO_ICMP;
ip_out->src_ip = orig_ip->dst_ip; // 10.0.0.2
ip_out->dst_ip = orig_ip->src_ip; // 10.0.0.1
ip_out->checksum = 0;
ip_out->checksum = ipv4_checksum(ip_out, sizeof(struct ipv4_hdr));
// 3. 組裝 ICMP 標頭 (Type 11, Code 0, Unused 填 0)
icmp_out->type = ICMP_TIME_EXCEEDED;
icmp_out->code = 0;
icmp_out->checksum = 0;
icmp_out->identifier = 0;
icmp_out->sequence = 0;
// 4. 複製原 IP 標頭 + 前 8 bytes 負載
memcpy(icmp_payload, orig_ip, icmp_data_len);
// 5. 計算 ICMP Checksum
icmp_out->checksum = ipv4_checksum(icmp_out, icmp_total_len);
// 6. 送出
ssize_t sent = write(fd, reply, reply_frame_len);
if (sent < 0) {
perror("[ICMP] write Time Exceeded failed");
} else {
printf("[ICMP] Time Exceeded (Type 11, Code 0) sent to ");
ipv4_print_ip(ip_out->dst_ip);
printf("\n");
fflush(stdout);
}
}
以及重構收納的 icmp_receive:
void icmp_receive(int fd, const uint8_t *frame, size_t len)
{
if (len < ETH_HEADER_LEN + sizeof(struct ipv4_hdr) + sizeof(struct icmp_hdr))
return;
const uint8_t *payload = frame + ETH_HEADER_LEN;
const struct ipv4_hdr *ip = (const struct ipv4_hdr *)payload;
uint8_t ihl = (ip->version_ihl & 0x0F) * 4;
const struct icmp_hdr *icmp_in = (const struct icmp_hdr *)(payload + ihl);
size_t icmp_in_len = len - ETH_HEADER_LEN - ihl;
icmp_print_header(icmp_in, icmp_in_len);
if (icmp_in->type != ICMP_ECHO_REQUEST) {
return;
}
uint8_t reply[2048];
memcpy(reply, frame, len);
struct ethernet_hdr *eth_out = (struct ethernet_hdr *)reply;
struct ipv4_hdr *ip_out = (struct ipv4_hdr *)(reply + ETH_HEADER_LEN);
uint8_t *icmp_out = reply + ETH_HEADER_LEN + ihl;
size_t icmp_len = len - ETH_HEADER_LEN - ihl;
// 交換 MAC 與 IP
uint8_t temp_mac[ETH_ADDR_LEN];
memcpy(temp_mac, eth_out->dst, ETH_ADDR_LEN);
memcpy(eth_out->dst, eth_out->src, ETH_ADDR_LEN);
memcpy(eth_out->src, temp_mac, ETH_ADDR_LEN);
uint32_t temp_ip = ip_out->dst_ip;
ip_out->dst_ip = ip_out->src_ip;
ip_out->src_ip = temp_ip;
ip_out->ttl = 64;
ip_out->checksum = 0;
ip_out->checksum = ipv4_checksum(ip_out, ihl);
icmp_handle(icmp_out, icmp_len);
ssize_t sent = write(fd, reply, len);
if (sent < 0) {
perror("[ICMP] write Echo Reply failed");
} else {
printf("[ICMP] Echo Reply sent (%ld bytes)\n", sent);
fflush(stdout);
}
}
src/tap.c:高度精簡的主分流邏輯在 src/tap.c 中,原本臃腫的程式碼變得優雅清晰:
case ETHERTYPE_IPV4:
if (payload_len >= sizeof(struct ipv4_hdr)) {
struct ipv4_hdr *ip = (struct ipv4_hdr *)payload;
ipv4_print_header(ip);
// Day 10 簡化模型:先模擬 Router forwarding path 的 TTL 扣減
// 真實 stack 會先判斷 Local Delivery;只有需要轉發的封包才扣 TTL
if (ipv4_decrement_ttl(ip) != 0) {
printf("[IPv4] TTL Expired\n");
icmp_send_time_exceeded(fd, buffer, n);
fflush(stdout);
break; // 跳出 switch,不往下處理 protocol
}
// 2. IPv4 Protocol Dispatcher
switch (ip->protocol) {
case IPPROTO_ICMP:
icmp_receive(fd, buffer, n); // 一行委託 ICMP 模組!
break;
default:
break;
}
}
break;
修改 test/send_icmp.c 中的 ip->ttl = 1:
# Terminal 1:
make
sudo ./network
# Terminal 2:
make send_icmp
sudo ./send_icmp
Terminal 1 輸出(驗收成功):
[ACCEPT]
Ethernet Frame
-------------------------
Destination : 02:00:00:00:00:01
Source : 52:54:00:12:34:56
EtherType : 0x0800
IPv4 Packet
------------------
Version : 4
Header Length: 20 bytes
TTL : 1
Protocol : 1
Checksum : 0xa5de (OK)
Source IP : 10.0.0.1
Destination IP : 10.0.0.2
[IPv4] TTL Expired
[ICMP] Time Exceeded (Type 11, Code 0) sent to 10.0.0.1
Frame length: 42 bytes
在 Host 端執行標準 Ping:
ping -c 1 10.0.0.2
Terminal 1 輸出:
[ACCEPT]
Ethernet Frame
...
IPv4 Packet (TTL = 64)
...
ICMP Packet (Type = 8, Code = 0)
...
[ICMP] Echo Request received -> Generating Echo Reply
[ICMP] Echo Reply sent (98 bytes)
重構後不但保留了原先的自動 Ping-Pong 機制,還無縫擴充了 TTL 逾期處理!
Network Stack Architecture (Day 10)
Ethernet (L2)
│
┌────────────────┴────────────────┐
│ │
ARP (0x0806) IPv4 (0x0800)
│ │
ARP Request / Reply IPv4 Checksum Verify
│ │
ARP Table TTL Check & Decrement
│
┌──────────┴──────────┐
▼ ▼
TTL == 0 TTL > 0
(Drop & Notify) (Protocol Dispatcher)
│ │
▼ ▼
ICMP Time Exceeded ICMP (Protocol = 1)
(Type = 11) │
┌────────┴────────┐
│ │
Echo Request Echo Reply
(Type = 8) (Type = 0)
到目前為止,我們的 Network Stack 都是假設:「所有收到的封包,目的地都是要給我的」。
但在真實的網際網路中,路由器每天面對的成千上萬個封包,目的地根本都不是自己!
下一天我們將踏出邁向「真實路由器」最重要的一步:Routing(路由選擇):